const isServer = typeof window === 'undefined'
const ieVersion = isServer ? 0 : Number((document as any).documentMode)
const SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g
const MOZ_HACK_REGEXP = /^moz([A-Z])/

export interface ViewportOffsetResult {
  left: number
  top: number
  right: number
  bottom: number
  rightIncludeBody: number
  bottomIncludeBody: number
}

/* istanbul ignore next */
const trim = function (string: string) {
  return (string || '').replace(/^\s+|\s+$/g, '')
}

/* istanbul ignore next */
const camelCase = function (name: string) {
  return name
    .replace(SPECIAL_CHARS_REGEXP, (_, __, letter, offset) => {
      return offset ? letter.toUpperCase() : letter
    })
    .replace(MOZ_HACK_REGEXP, 'Moz$1')
}

/* istanbul ignore next */
export function hasClass(el: Element, cls: string) {
  if (!el || !cls)
    return false
  if (cls.includes(' '))
    throw new Error('className should not contain space.')

  if (el.classList)
    return el.classList.contains(cls)
  else return ` ${el.className} `.includes(` ${cls} `)
}

/* istanbul ignore next */
export function addClass(el: Element, cls: string) {
  if (!el)
    return
  let curClass = el.className
  const classes = (cls || '').split(' ')

  for (let i = 0, j = classes.length; i < j; i++) {
    const clsName = classes[i]
    if (!clsName)
      continue

    if (el.classList)
      el.classList.add(clsName)
    else if (!hasClass(el, clsName))
      curClass += ` ${clsName}`
  }
  if (!el.classList)
    el.className = curClass
}

/* istanbul ignore next */
export function removeClass(el: Element, cls: string) {
  if (!el || !cls)
    return
  const classes = cls.split(' ')
  let curClass = ` ${el.className} `

  for (let i = 0, j = classes.length; i < j; i++) {
    const clsName = classes[i]
    if (!clsName)
      continue

    if (el.classList)
      el.classList.remove(clsName)
    else if (hasClass(el, clsName))
      curClass = curClass.replace(` ${clsName} `, ' ')
  }
  if (!el.classList)
    el.className = trim(curClass)
}

export function getBoundingClientRect(element: Element): DOMRect | number {
  if (!element || !element.getBoundingClientRect)
    return 0

  return element.getBoundingClientRect()
}

/**
 * 获取当前元素的left、top偏移
 *   left：元素最左侧距离文档左侧的距离
 *   top:元素最顶端距离文档顶端的距离
 *   right:元素最右侧距离文档右侧的距离
 *   bottom：元素最底端距离文档底端的距离
 *   rightIncludeBody：元素最左侧距离文档右侧的距离
 *   bottomIncludeBody：元素最底端距离文档最底部的距离
 */
export function getViewportOffset(element: Element): ViewportOffsetResult {
  const doc = document.documentElement

  const docScrollLeft = doc.scrollLeft
  const docScrollTop = doc.scrollTop
  const docClientLeft = doc.clientLeft
  const docClientTop = doc.clientTop

  const pageXOffset = window.pageXOffset
  const pageYOffset = window.pageYOffset

  const box = getBoundingClientRect(element)

  const {
    left: retLeft,
    top: rectTop,
    width: rectWidth,
    height: rectHeight,
  } = box as DOMRect

  const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0)
  const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0)
  const offsetLeft = retLeft + pageXOffset
  const offsetTop = rectTop + pageYOffset

  const left = offsetLeft - scrollLeft
  const top = offsetTop - scrollTop

  const clientWidth = window.document.documentElement.clientWidth
  const clientHeight = window.document.documentElement.clientHeight
  return {
    left,
    top,
    right: clientWidth - rectWidth - left,
    bottom: clientHeight - rectHeight - top,
    rightIncludeBody: clientWidth - left,
    bottomIncludeBody: clientHeight - top,
  }
}

/* istanbul ignore next */
export const on = function (
  element: HTMLElement | Document | Window,
  event: string,
  handler: EventListenerOrEventListenerObject,
): void {
  if (element && event && handler)
    element.addEventListener(event, handler, false)
}

/* istanbul ignore next */
export const off = function (
  element: HTMLElement | Document | Window,
  event: string,
  handler: any,
): void {
  if (element && event && handler)
    element.removeEventListener(event, handler, false)
}

/* istanbul ignore next */
export const once = function (
  el: HTMLElement,
  event: string,
  fn: EventListener,
): void {
  const listener = function (this: any, ...args: unknown[]) {
    if (fn)
      fn.apply(this, args)

    off(el, event, listener)
  }
  on(el, event, listener)
}

/* istanbul ignore next */
export const getStyle
  = ieVersion < 9
    ? function (element: Element | any, styleName: string) {
      if (isServer)
        return
      if (!element || !styleName)
        return null
      styleName = camelCase(styleName)
      if (styleName === 'float')
        styleName = 'styleFloat'

      try {
        switch (styleName) {
          case 'opacity':
            try {
              return element.filters.item('alpha').opacity / 100
            }
            catch {
              return 1.0
            }
          default:
            return element.style[styleName] || element.currentStyle
              ? element.currentStyle[styleName]
              : null
        }
      }
      catch {
        return element.style[styleName]
      }
    }
    : function (element: Element | any, styleName: string) {
      if (isServer)
        return
      if (!element || !styleName)
        return null
      styleName = camelCase(styleName)
      if (styleName === 'float')
        styleName = 'cssFloat'

      try {
        const computed = (document as any).defaultView.getComputedStyle(
          element,
          '',
        )
        return element.style[styleName] || computed
          ? computed[styleName]
          : null
      }
      catch {
        return element.style[styleName]
      }
    }

/* istanbul ignore next */
export function setStyle(element: Element | any, styleName: any, value: any) {
  if (!element || !styleName)
    return

  if (typeof styleName === 'object') {
    for (const prop in styleName) {
      if (Object.prototype.hasOwnProperty.call(styleName, prop))
        setStyle(element, prop, styleName[prop])
    }
  }
  else {
    styleName = camelCase(styleName)
    if (styleName === 'opacity' && ieVersion < 9) {
      element.style.filter = Number.isNaN(value)
        ? ''
        : `alpha(opacity=${value * 100})`
    }
    else {
      element.style[styleName] = value
    }
  }
}

/* istanbul ignore next */
export function isScroll(el: Element, vertical: any) {
  if (isServer)
    return

  const determinedDirection = vertical !== null || vertical !== undefined
  const overflow = determinedDirection
    ? vertical
      ? getStyle(el, 'overflow-y')
      : getStyle(el, 'overflow-x')
    : getStyle(el, 'overflow')

  return overflow.match(/(scroll|auto)/)
}

/* istanbul ignore next */
export function getScrollContainer(el: Element, vertical?: any) {
  if (isServer)
    return

  let parent: any = el
  while (parent) {
    if ([window, document, document.documentElement].includes(parent))
      return window

    if (isScroll(parent, vertical))
      return parent

    parent = parent.parentNode
  }

  return parent
}

/* istanbul ignore next */
export function isInContainer(el: Element, container: any) {
  if (isServer || !el || !container)
    return false

  const elRect = el.getBoundingClientRect()
  let containerRect

  if (
    [window, document, document.documentElement, null, undefined].includes(
      container,
    )
  ) {
    containerRect = {
      top: 0,
      right: window.innerWidth,
      bottom: window.innerHeight,
      left: 0,
    }
  }
  else {
    containerRect = container.getBoundingClientRect()
  }

  return (
    elRect.top < containerRect.bottom
    && elRect.bottom > containerRect.top
    && elRect.right > containerRect.left
    && elRect.left < containerRect.right
  )
}

/**
 * copyText 函数，它接受一个字符串作为输入，并返回一个 Promise 对象表示复制操作的异步结果
 */
export function copyText(text: string, useTextarea?: boolean): Promise<void> {
  if (window.navigator?.clipboard && !useTextarea) {
    return new Promise((resolve, reject) => {
      navigator.clipboard
        .writeText(text)
        .then(() => resolve())
        .catch(error => reject(error))
    })
  }
  else {
    return new Promise((resolve, reject) => {
      const textarea = document.createElement('textarea')
      textarea.value = text
      document.body.appendChild(textarea)

      textarea.select()
      textarea.setSelectionRange(0, text.length)

      let success = false
      try {
        success = document.execCommand('copy')
      }
      catch (error) {
        reject(error)
      }

      document.body.removeChild(textarea)

      if (success)
        resolve()
      else reject(new Error('文本复制失败'))
    })
  }
}

export function setCssVar(
  prop: string,
  val: string,
  dom = document.documentElement,
) {
  if (val.includes('!important')) {
    dom.style.setProperty(prop, val.replaceAll('!important', ''), 'important')
  }
  else {
    dom.style.setProperty(prop, val)
  }
}
